Skip to content

06 复合类型类型 - 结构体的使用和注意事项

  • 结构体(struct)是一种复合数据类型,用于将不同类型的数据组合成一个整体。
  • 它可以用来定义复杂的数据模型,并且在编写面向对象风格的代码时非常有用。
  • 通过结构体,可以更好地表示具有多个属性的实体,并将这些属性封装在一起。

定义结构体

在 Go 中,使用 type 关键字来定义一个结构体类型。结构体的定义通常放在全局作用域。

go
package main

import "fmt"

// 定义一个结构体类型
type Person struct {
    Name string
    Age  int
    City string
}

func main() {
    // 创建结构体实例
    var p1 Person  // 使用零值初始化
    p1.Name = "Alice"
    p1.Age = 30
    p1.City = "New York"

    fmt.Println(p1)  // 输出:{Alice 30 New York}

    // 使用结构体字面量创建
    p2 := Person{Name: "Bob", Age: 25, City: "San Francisco"}
    fmt.Println(p2)  // 输出:{Bob 25 San Francisco}
}
  • 结构体字段(例如 Name, Age, City)可以是任何数据类型,包括基本类型(如 stringint)、数组、切片、指针、甚至是其他结构体类型。
  • 字段名必须以大写字母开头才能被其他包访问(导出字段),如果是小写字母开头,则只能在当前包中使用(未导出字段)。

结构体的实例化

go
type Person struct {
    Name string
    Age  int
    City string
}
  1. 当声明一个结构体变量时,如果没有显式初始化,则所有字段都会被设置为该字段类型的默认值。

    go
    var p1 Person  // 所有字段都被设置为默认值("",0,nil 等)
    fmt.Println(p1)  // 输出:{ 0 }
  2. 使用结构体字面量来初始化。

    go
    p2 := Person{"Charlie", 22, "Seattle"}
    fmt.Println(p2)  // 输出:{Charlie 22 Seattle}

    或者使用命名字段的方式(推荐),这样初始化时可以按任意顺序指定字段。

    go
    p3 := Person{Name: "David", City: "Los Angeles"}
    fmt.Println(p3)  // 输出:{David 0 Los Angeles} (未指定的字段使用默认值)
  3. 使用 new 关键字可以创建结构体的指针实例。(很少使用,不推荐!)

    go
    p4 := new(Person)
    p4.Name = "Eve"
    p4.Age = 28
    p4.City = "Boston"
    fmt.Println(*p4)  // 输出:{Eve 28 Boston}
  4. 使用 & 操作符来创建结构体的指针。

    go
    p5 := &Person{Name: "Frank", Age: 35, City: "Chicago"}
    fmt.Println(*p5)  // 输出:{Frank 35 Chicago}

结构体字段的访问与修改

可以使用点操作符(.)来访问和修改结构体字段。

go
p := Person{Name: "Grace", Age: 40, City: "New York"}
fmt.Println(p.Name)  // 输出:Grace

// 修改字段值
p.Age = 41
fmt.Println(p.Age)  // 输出:41

对于结构体指针,也可以直接使用点操作符访问字段。

go
p := &Person{Name: "Hank", Age: 45, City: "Los Angeles"}
fmt.Println(p.Age)  // 输出:45(自动解引用)

嵌套结构体

  • 结构体可以包含其他结构体作为字段,从而形成嵌套结构。
  • 使用嵌套结构体可以更好地组织和表示复杂的数据关系。
go
type Address struct {
    Street string
    City   string
    Zip    string
}

type User struct {
    Name    string
    Age     int
    Contact Address
}

func main() {
    user := User{
        Name: "John",
        Age:  30,
        Contact: Address{
            Street: "123 Main St",
            City:   "New York",
            Zip:    "10001",
        },
    }

    fmt.Println(user)          // 输出:{John 30 {123 Main St New York 10001}}
    fmt.Println(user.Contact)  // 输出:{123 Main St New York 10001}
    fmt.Println(user.Contact.City)  // 输出:New York
}

匿名字段与结构体嵌入

Go 中允许在结构体中使用匿名字段(嵌入其他结构体类型),这可以被认为是 Go 中的一种简化的继承方式。

go
type Person struct {
    Name string
    Age  int
}

type Employee struct {
    Person  // 匿名字段,嵌入了 Person 结构体
    ID      int
    Company string
}

func main() {
    emp := Employee{
        Person: Person{Name: "Mike", Age: 29},
        ID:     1001,
        Company: "Tech Corp",
    }
    fmt.Println(emp)  // 输出:{{Mike 29} 1001 Tech Corp}

    // 直接访问嵌入字段
    fmt.Println(emp.Name)  // 输出:Mike
    fmt.Println(emp.Age)   // 输出:29
}
  • Employee 嵌入了 Person 结构体,可以直接访问 Person 的字段(例如 NameAge),就像它们是 Employee 自身的字段一样。

方法与结构体

结构体可以绑定方法,使其具备类似面向对象编程中的“类”功能。方法的接收者(receiver)可以是结构体的值类型或指针类型。

go
type Rectangle struct {
    Width  float64
    Height float64
}

// 绑定值类型的接收者
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// 绑定指针类型的接收者
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    fmt.Println("Area:", rect.Area())  // 输出:Area: 50

    rect.Scale(2)
    fmt.Println("Scaled Area:", rect.Area())  // 输出:Scaled Area: 200
}
  • 值接收者:方法接收结构体的副本,不会影响原结构体。
  • 指针接收者:方法接收结构体的指针,可以修改原结构体的内容。

比较结构体

在 Go 中,结构体只能进行相等性比较(==!=),前提是所有字段都是可比较的类型。如果两个结构体的字段值完全相同,则它们被认为是相等的。

go
type Point struct {
    X, Y int
}

p1 := Point{X: 1, Y: 2}
p2 := Point{X: 1, Y: 2}
fmt.Println(p1 == p2)  // 输出:true

结构体标签

Go 结构体可以使用标签(tag)为字段添加元数据,常用于 JSON 序列化、数据库映射等场景。

go
type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

import "encoding/json"

u := User{Name: "Tom", Age: 20}
jsonData, _ := json.Marshal(u)
fmt.Println(string(jsonData))  // 输出:{"name":"Tom","age":20}

结构体的默认值

结构体的零值是其字段类型的默认值。即使没有初始化字段,结构体依然可以正常使用。

go
type MyStruct struct {
    Name string
    Age  int
    Valid bool
}

var s MyStruct
fmt.Println(s)  // 输出:{ 0 false}

结构体指针

  • 结构体是值类型。这意味着当你将一个结构体赋值给另一个变量时,实际上是创建了这个结构体的一个副本,而不是引用原始结构体。
  • 为了避免这种副本操作,可以使用指针来引用结构体,从而节省内存,并且可以直接修改原结构体的数据。

  • & 操作符:获取变量的指针(地址)。
  • * 操作符:通过指针访问变量的值。
go
package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func main() {
    // 使用值类型创建结构体
    p1 := Person{Name: "Alice", Age: 25}
    p2 := p1          // p2 是 p1 的副本
    p2.Age = 30       // 修改 p2 的 Age 字段,不会影响 p1
    fmt.Println(p1)   // 输出:{Alice 25}
    fmt.Println(p2)   // 输出:{Alice 30}

    // 使用结构体指针
    p3 := &p1         // p3 是 p1 的指针
    p3.Age = 35       // 修改 p3 的 Age 字段,会影响 p1
    fmt.Println(p1)   // 输出:{Alice 35}
    fmt.Println(*p3)  // 输出:{Alice 35}
}

使用 & 创建结构体指针

可以使用 & 操作符来创建一个结构体的指针。这样创建的指针变量可以直接操作结构体的字段。

go
type Rectangle struct {
    Width  float64
    Height float64
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    rectPointer := &rect        // 使用 & 操作符获取结构体指针
    fmt.Println(rectPointer)    // 输出:&{10 5}

    // 修改指针指向的结构体字段
    rectPointer.Width = 20
    fmt.Println(rect)           // 输出:{20 5}
}

使用 new 创建结构体指针

Go 提供了 new 关键字用于创建结构体指针实例。new 函数分配内存并返回结构体的指针,但不会进行初始化。

go
type Circle struct {
    Radius float64
}

func main() {
    c := new(Circle)  // 返回 *Circle 类型的指针
    fmt.Println(c)    // 输出:&{0} (字段未初始化)

    // 通过指针修改字段
    c.Radius = 10
    fmt.Println(*c)   // 输出:{10}
}

**`new` 和 `&` 的区别**

  • new 仅分配内存并返回指针,所有字段都被设置为零值。
  • 使用 & 取地址时,可以对结构体字段进行初始化。

结构体指针的字段访问

当你有一个结构体指针时,可以通过 . 操作符直接访问和修改它的字段,而不需要显式地使用 * 来解引用(Go 编译器会自动处理解引用操作)。

go
package main

import "fmt"

type Book struct {
    Title  string
    Author string
}

func main() {
    book := Book{Title: "Go Programming", Author: "Alice"}
    bookPtr := &book   // 获取结构体的指针

    // 使用指针访问和修改字段
    // bookPtr.Title 实际上等价于 (*bookPtr).Title
    // Go 会自动解引用结构体指针,简化代码书写
    bookPtr.Title = "Advanced Go"
    fmt.Println(book)  // 输出:{Advanced Go Alice}
}

结构体指针作为函数参数

使用结构体指针作为函数参数,可以避免结构体的副本拷贝,并且可以在函数中修改原结构体的数据。

go
package main

import "fmt"

type Employee struct {
    Name string
    Age  int
}

// 值传递(不会修改原结构体)
func updateAgeByValue(e Employee) {
    e.Age = 50
}

// 指针传递(会修改原结构体)
func updateAgeByPointer(e *Employee) {
    e.Age = 50
}

func main() {
    emp := Employee{Name: "John", Age: 30}

    updateAgeByValue(emp)
    fmt.Println(emp)  // 输出:{John 30} (没有变化)

    updateAgeByPointer(&emp)
    fmt.Println(emp)  // 输出:{John 50} (发生变化)
}
  • updateAgeByValue 函数中,传入的是结构体的副本,所以对 e.Age 的修改不会影响原结构体。
  • updateAgeByPointer 函数中,传入的是结构体的指针,因此修改指针指向的字段,会影响原结构体。

结构体方法中的指针接收者与值接收者

在结构体方法中,可以选择使用值接收者或指针接收者。指针接收者允许修改结构体的内容,而值接收者则是结构体的副本,无法修改原始数据。

go
package main

import "fmt"

type Rectangle struct {
    Width  float64
    Height float64
}

// 值接收者方法(不会修改原始数据)
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// 指针接收者方法(可以修改原始数据)
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}

    // 使用值接收者调用方法(不会改变原结构体)
    fmt.Println("Area:", rect.Area())  // 输出:Area: 50

    // 使用指针接收者调用方法(改变了原结构体)
    rect.Scale(2)
    fmt.Println("Scaled Rectangle:", rect)  // 输出:Scaled Rectangle: {20 10}
}
  • 当一个方法使用值接收者时,它只能访问和操作结构体的副本。
  • 当一个方法使用指针接收者时,它可以直接修改结构体本身。

指针接收者的最佳实践

一般来说,使用指针接收者有以下场景:

  1. 结构体很大,传递指针比值拷贝更高效
  2. 需要修改原结构体的内容
  3. 保持一致性:如果一个方法需要指针接收者,那么所有方法都应该使用指针接收者